/**
 *  Copyright (C) 2011 Federico Bergstein
 *
 *  gwt isometric board is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  gwt isometric board is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with gwt isometric board Project.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package gwt.gwtisometricboard.client;

import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.dom.client.ImageElement;

import gwt.gwtisometricboard.client.model.Thing;
import gwt.gwtisometricboard.client.model.Tile;

/**
 * 
 * Draws an isometric game board on a given surface.
 * 
 * @author Federico Bergstein
 * 
 */
public class BoardDrawer {

	// fields
	private Context2d context;
	private ImageProvider imageProvider;
	private int boardHeight = 0;
	private int boardWidth = 0;
	private int tileImageHeight = 0;
	private int zoom = 100;

	private int tileImageWidth = 0;
	private Tile[] tiles;
	private boolean drawBorders = false;
	private boolean gridLines = false;
	private boolean drawTileCoordinates = false;
	private int displayCoordinateXOffset = 0;
	private int displayCoordinateYOffset = 0;

	// local variables
	private Tile[][] tileMap;
	int centralY = 0;
	int centralX = 0;

	// Constructors

	/**
	 * 
	 * The class constructor
	 * 
	 * @param tiles
	 *            the tiles of the board.
	 * @param surface
	 *            the surface to draw the board on.
	 * @param imageProvider
	 *            an implementation of the ImageProvider interface, to provide
	 *            the different images for all the objects in the board
	 *            (tiles,borders,things/pieces,etc).
	 * @param boardHeight
	 *            the board's height in tiles.
	 * @param boardWidth
	 *            the board's width in tiles.
	 * @param tileImageHeight
	 *            the height of the tile's image.
	 * @param tileImageWidth
	 *            the width of the tile's image.
	 * @param drawBorders
	 *            whether or not to draw borders for each tile.
	 * @param gridLines
	 *            whether or not to draw grid lines in the board.
	 * @param drawTileCoordinates
	 *            whether or not to draw each tile's coordinates on it.
	 */
	public BoardDrawer(Tile[] tiles, Context2d context,
			ImageProvider imageProvider, int boardHeight, int boardWidth,
			int tileImageHeight, int tileImageWidth, boolean drawBorders,
			boolean gridLines, boolean drawTileCoordinates) {
		this.context = context;
		this.imageProvider = imageProvider;
		this.boardHeight = boardHeight;
		this.boardWidth = boardWidth;
		this.tileImageHeight = tileImageHeight;
		this.tileImageWidth = tileImageWidth;
		this.tiles = tiles;
		this.drawBorders = drawBorders;
		this.drawTileCoordinates = drawTileCoordinates;
		this.gridLines = gridLines;

	}

	// public properties

	/**
	 * @param boardHeight
	 *            the number of tiles in the board's y axis.
	 */
	public void setBoardHeight(int boardHeight) {
		this.boardHeight = boardHeight;
	}

	/**
	 * @param boardWidth
	 *            the number of tiles in the board's x axis.
	 */
	public void setBoardWidth(int boardWidth) {
		this.boardWidth = boardWidth;
	}

	/**
	 * @param tileHeight
	 *            the height of the tile's image.
	 */
	public void setTileImageHeight(int tileHeight) {
		this.tileImageHeight = tileHeight;
	}
	
	
	/**
	 * @return the Height of the Tile Image
	 */
	public int getTileImageHeight() {
		return tileImageHeight;
	}

	/**
	 * @param tileWidth
	 *            the width of the tile's image.
	 */
	public void setTileImageWidth(int tileWidth) {
		this.tileImageWidth = tileWidth;
	}
	
	/**
	 * @return the Width of the Tile Image
	 */
	public int getTileImageWidth() {
		return tileImageWidth;
	}

	/**
	 * @param imageProvider
	 *            the imageProvider to use as a source for the images.
	 */
	public void setImageProvider(ImageProvider imageProvider) {
		this.imageProvider = imageProvider;
	}

	/**
	 * @param zoom the zoom to set
	 */
	public void setZoom(int zoom) {
		this.zoom = zoom;
	}

	/**
	 * @return the zoom
	 */
	public int getZoom() {
		return zoom;
	}

	/**
	 * @param surface
	 *            the surface to draw the board on.
	 */
	public void setSurface(Context2d surface) {
		this.context = surface;
	}

	/**
	 * @return true if the tile border images will be drawn.
	 */
	public boolean drawsBorders() {
		return drawBorders;
	}

	/**
	 * @param drawBorders
	 *            sets whether the tile border images will be drawn.
	 */
	public void setDrawBorders(boolean drawBorders) {
		this.drawBorders = drawBorders;
	}

	/**
	 * @param gridLines
	 *            sets whether the grid lines will be drawn.
	 */
	public void setGridLines(boolean gridLines) {
		this.gridLines = gridLines;
	}

	/**
	 * @return true if the grid lines will be drawn.
	 */
	public boolean drawsGridLines() {
		return gridLines;
	}

	/**
	 * @param drawTileCoordinates
	 *            sets whether the tile coordinates will be drawn.
	 */
	public void setDrawTileCoordinates(boolean drawTileCoordinates) {
		this.drawTileCoordinates = drawTileCoordinates;
	}

	/**
	 * @return true if the Tile Coordinates will be drawn.
	 */
	public boolean drawsTileCoordinates() {
		return drawTileCoordinates;
	}

	/**
	 * @param the
	 *            offset to add to the x Coordinate displayed in the tiles
	 */
	public void setDisplayCoordinateXOffset(int displayCoordinateXOffset) {
		this.displayCoordinateXOffset = displayCoordinateXOffset;
	}

	/**
	 * @return the offset to add to the x Coordinate displayed in the tiles
	 */
	public int getDisplayCoordinateXOffset() {
		return displayCoordinateXOffset;
	}

	/**
	 * @param displayCoordinateYOffset
	 *            the offset to add to the y Coordinate displayed in the tiles
	 */
	public void setDisplayCoordinateYOffset(int displayCoordinateYOffset) {
		this.displayCoordinateYOffset = displayCoordinateYOffset;
	}

	/**
	 * @return the offset to add to the y Coordinate displayed in the tiles
	 */
	public int getDisplayCoordinateYOffset() {
		return displayCoordinateYOffset;
	}

	// Public Methods

	/**
	 * Draws the board.
	 */
	public void drawBoard() {

		tileMap = new Tile[boardWidth][boardHeight];

		centralY = ((boardHeight / 2) * getZoomedTileImageHeight()) + getZoomedTileImageHeight() / 2;
		centralX = getZoomedTileImageWidth() / 2;

		for (int i = 0; i < tiles.length; i++) {
			tileMap[tiles[i].getX()][tiles[i].getY()] = tiles[i];
		}

		for (int i = 0; i < tiles.length; i++) {
			drawTile(tiles[i]);
		}

		for (int i = 0; i < tiles.length; i++) {
			drawThings(tiles[i]);
		}
	}

	/**
	 * 
	 * Returns the board tile that contains the screen coordinates.
	 * 
	 * @param x
	 *            the x screen coordinate
	 * @param y
	 *            the y screen coordinate
	 * @return
	 * 			  the tile that contains the screen coordinates
	 */			  
	public Tile getTileFromScreenCoordinates(int x, int y) {

		for (int i = 0; i < tiles.length; i++) {

			if (tileContains(tiles[i], x, y)) {
				return tiles[i];
			}
		}
		return null;
	}

	// Private Methods

	// determines the center of a tile.
	private Point getTileCenterXY(Tile tile) {
		Point result = new Point(centralX
				+ ((tile.getY() + tile.getX()) * (getZoomedTileImageWidth() / 2)),
				centralY
						+ ((tile.getY() - tile.getX()) * (getZoomedTileImageHeight() / 2)));
		return result;

	}

	// draws all the things in a given tile.
	private void drawThings(Tile tile) {

		Point center = getTileCenterXY(tile);

		if (tile.getThingList() != null) {
			for (Thing thing : tile.getThingList()) {
				ImageElement image = imageProvider.getThingImage(thing);

				
				context.drawImage(image, center.getX() - zoomedDimension(image.getWidth()) / 2,
						center.getY() - zoomedDimension(image.getHeight()), zoomedDimension(image.getWidth()),
						 zoomedDimension(image.getHeight()));
			}
		}

	}

	// draws a tile, it's border and it's coordinates
	private void drawTile(Tile tile) {

		Point center = getTileCenterXY(tile);

		Point[] points = calculateTilePoints(center);

		if (gridLines) {

			context.beginPath();
			context.moveTo(points[0].getX(), points[0].getY());
			context.lineTo(points[1].getX(), points[1].getY());
			context.lineTo(points[2].getX(), points[2].getY());
			context.lineTo(points[3].getX(), points[3].getY());
			context.lineTo(points[0].getX(), points[0].getY());
			context.stroke();
			context.closePath();
			
		}

		ImageElement image = imageProvider.getCenter(tile);
		 context.drawImage(image, points[0].getX(), points[1].getY(), getZoomedTileImageWidth(), getZoomedTileImageHeight());

		if (drawBorders) {
			drawBorderImages(tile, points[0].getX(), points[1].getY());

		}

		if (drawTileCoordinates) {
			context.strokeText(String.valueOf(tile.getX()
					+ displayCoordinateXOffset)
					+ ","
					+ String.valueOf(tile.getY() + displayCoordinateYOffset),
					center.getX(), center.getY());
		}

	}

	/**
	 * draws all the border images of a given tile
	 * 
	 * @param tile
	 *            the tile whose borders will be drawn.
	 * @param tilePositionX
	 *            the x coordinate of the center of the tile.
	 * @param tilePositionY
	 *            the y coordinate of the center of the tile.
	 */
	private void drawBorderImages(Tile tile, int tilePositionX,
			int tilePositionY) {

		ImageElement[] borders = new ImageElement[8];

		borders[0] = imageProvider.getBorder(tile, getTile(tile.getX() - 1,
				tile.getY() - 1));

		borders[1] = imageProvider.getBorder(tile, getTile(tile.getX(), tile
				.getY() - 1));

		borders[2] = imageProvider.getBorder(tile, getTile(tile.getX() + 1,
				tile.getY() - 1));

		borders[3] = imageProvider.getBorder(tile, getTile(tile.getX() - 1,
				tile.getY() + 1));

		borders[4] = imageProvider.getBorder(tile, getTile(tile.getX(), tile
				.getY() + 1));

		borders[5] = imageProvider.getBorder(tile, getTile(tile.getX() + 1,
				tile.getY() + 1));

		borders[6] = imageProvider.getBorder(tile, getTile(tile.getX() + 1,
				tile.getY()));

		borders[7] = imageProvider.getBorder(tile, getTile(tile.getX() - 1,
				tile.getY()));

		for (int i = 0; i < borders.length; i++) {
			if (borders[i] != null) {
				context.drawImage(borders[i], tilePositionX, tilePositionY,
						zoomedDimension(borders[i].getWidth()), zoomedDimension(borders[i].getHeight()));
			}
		}

	}

	// returns a tile from it's coordinates.
	private Tile getTile(int x, int y) {
		if (x >= 0 && y >= 0 && x < boardWidth && y < boardHeight) {
			return tileMap[x][y];
		}
		return null;
	}

	/**
	 * Returns true if the x,y coordinates are inside the polygon defined by the
	 * xp and yp arrays.
	 * 
	 * @param xp
	 *            the x coordinates of the points of the polygon.
	 * @param yp
	 *            the y coordinates of the points of the polygon.
	 * @param x
	 *            x coordinate of the point.
	 * @param y
	 *            y coordinate of the point.
	 * @return
	 */
	private boolean pnpoly(int[] xp, int[] yp, int x, int y) {
		int i, j, npol = xp.length;

		boolean c = false;
		for (i = 0, j = npol - 1; i < npol; j = i++) {
			if ((((yp[i] <= y) && (y < yp[j])) || ((yp[j] <= y) && (y < yp[i])))
					&& (x < (xp[j] - xp[i]) * (y - yp[i]) / (yp[j] - yp[i])
							+ xp[i])) {
				c = !c;
			}
		}
		return c;
	}

	/**
	 * 
	 * Calculates the outline of the tile given it's center.
	 * 
	 * @param tileCenter
	 *            the center position of the tile.
	 * @return 4 points array with all the points of the tile polygon.
	 */
	private Point[] calculateTilePoints(Point tileCenter) {
		Point[] result = new Point[4];
		result[0] = new Point(tileCenter.getX() - getZoomedTileImageWidth() / 2,
				tileCenter.getY());
		result[1] = new Point(tileCenter.getX(), tileCenter.getY()
				- getZoomedTileImageHeight() / 2);
		result[2] = new Point(tileCenter.getX() + getZoomedTileImageWidth() / 2,
				tileCenter.getY());
		result[3] = new Point(tileCenter.getX(), tileCenter.getY()
				+ getZoomedTileImageHeight() / 2);

		return result;

	}

	/**
	 * Determines if a point in the surface is contained by a tile
	 * 
	 * @param tile
	 *            the tile to check.
	 * @param x
	 *            coordinate of the point.
	 * @param y
	 *            coordinate of the point.
	 * @return true if the point is inside the tile, false if not.
	 */
	private boolean tileContains(Tile tile, int x, int y) {

		Point center = getTileCenterXY(tile);

		Point[] points = calculateTilePoints(center);

		int[] xp = new int[4];
		int[] yp = new int[4];
		xp[0] = points[0].getX();
		xp[1] = points[1].getX();
		xp[2] = points[2].getX();
		xp[3] = points[3].getX();

		yp[0] = points[0].getY();
		yp[1] = points[1].getY();
		yp[2] = points[2].getY();
		yp[3] = points[3].getY();

		return pnpoly(xp, yp, x, y);

	}
	
	private int getZoomedTileImageHeight()
	{
		return zoomedDimension(tileImageHeight);
	}
	
	private int getZoomedTileImageWidth()
	{
		return zoomedDimension(tileImageWidth);
	}
	
	private int zoomedDimension(int dimensionValue)
	{
		return dimensionValue * zoom / 100;
	}

	/**
	 * @param tiles the tiles to set
	 */
	public void setTiles(Tile[] tiles) {
		this.tiles = tiles;
	}

	/**
	 * @return the tiles
	 */
	public Tile[] getTiles() {
		return tiles;
	}

}
